Рабочий черновик (draftbook)
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import plotly.express as px
import plotly.graph_objects as go
import os
# Локальная папка с датасетами, если есть
!ls data
ExchangeRates.zip dst-3.0_16_1_hh_database.zip hh_data.zip
# Загрузка данных в зависимости от среды выполнения (vscode, jupiter notebook, collab.net)
file_id = '16I38xoQDFIQIAQwYPW60M53xaBsdqSzI'
google_drive_url='https://drive.google.com/uc?export=download&confirm=no_antivirus&id='
local_file = './data/hh_data.zip'
remote_url = google_drive_url + file_id
# есть локальный - берем локальный, нет - тянем с гугл диска
if os.path.exists('./data') and os.path.exists(local_file):
print('Load local:', local_file)
url = local_file
else:
print('Load remote:', remote_url)
url = remote_url
# Ускорим перезагрузку если уже "стянули"
hh_df : pd.DataFrame
if 'hh_df' in globals():
hh_data = hh_df.copy()
print('Reload copy from memory')
else:
hh_df = pd.read_csv(url, compression='zip', sep=';')
hh_data = hh_df.copy()
print('Loaded')
Load local: ./data/hh_data.zip Loaded
# Информация по столбцам
hh_data.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 44744 entries, 0 to 44743 Data columns (total 23 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 Ищет работу на должность: 44744 non-null object 1 Последнее/нынешнее место работы 44743 non-null object 2 Последняя/нынешняя должность 44742 non-null object 3 Обновление резюме 44744 non-null object 4 Авто 44744 non-null object 5 Образование 44744 non-null object 6 Пол 44744 non-null object 7 Возраст 44744 non-null int64 8 Опыт работы (месяц) 44574 non-null float64 9 Город 44744 non-null object 10 Готовность к переезду 44744 non-null bool 11 Готовность к командировкам 44744 non-null bool 12 проектная работа 44744 non-null bool 13 волонтерство 44744 non-null bool 14 полная занятость 44744 non-null bool 15 частичная занятость 44744 non-null bool 16 стажировка 44744 non-null bool 17 сменный график 44744 non-null bool 18 полный день 44744 non-null bool 19 гибкий график 44744 non-null bool 20 вахтовый метод 44744 non-null bool 21 удаленная работа 44744 non-null bool 22 ЗП (руб) 44744 non-null float64 dtypes: bool(12), float64(2), int64(1), object(8) memory usage: 4.3+ MB
# Размер, пропуски и статистика
print('Row, Col:', hh_data.shape)
print('NaN`s Column')
for colm in hh_data.columns:
cnt_na = hh_data[colm].isna().sum()
if cnt_na: print(f'{cnt_na:5} ', colm)
#
display(hh_data.describe())
display(hh_data.describe(include='object'))
display(hh_data.describe(include='bool'))
Row, Col: (44744, 23)
NaN`s Column
1 Последнее/нынешнее место работы
2 Последняя/нынешняя должность
170 Опыт работы (месяц)
| Возраст | Опыт работы (месяц) | ЗП (руб) | |
|---|---|---|---|
| count | 44744.000000 | 44574.000000 | 4.474400e+04 |
| mean | 32.196741 | 114.418944 | 7.652146e+04 |
| std | 7.929800 | 79.047861 | 1.359203e+05 |
| min | 14.000000 | 1.000000 | 1.000000e+00 |
| 25% | 27.000000 | 57.000000 | 3.708220e+04 |
| 50% | 31.000000 | 100.000000 | 5.900000e+04 |
| 75% | 36.000000 | 154.000000 | 9.500000e+04 |
| max | 100.000000 | 1188.000000 | 2.430488e+07 |
| Ищет работу на должность: | Последнее/нынешнее место работы | Последняя/нынешняя должность | Обновление резюме | Авто | Образование | Пол | Город | |
|---|---|---|---|---|---|---|---|---|
| count | 44744 | 44743 | 44742 | 44744 | 44744 | 44744 | 44744 | 44744 |
| unique | 14929 | 30214 | 16927 | 18838 | 2 | 4 | 2 | 4 |
| top | Системный администратор | Индивидуальное предпринимательство / частная п... | Системный администратор | 07.05.2019 09:50 | Не указано | высшее | М | Москва |
| freq | 3099 | 935 | 2062 | 25 | 32268 | 33863 | 36211 | 16621 |
| Готовность к переезду | Готовность к командировкам | проектная работа | волонтерство | полная занятость | частичная занятость | стажировка | сменный график | полный день | гибкий график | вахтовый метод | удаленная работа | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| count | 44744 | 44744 | 44744 | 44744 | 44744 | 44744 | 44744 | 44744 | 44744 | 44744 | 44744 | 44744 |
| unique | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 |
| top | False | True | False | False | True | False | False | False | True | False | False | False |
| freq | 28719 | 31646 | 36676 | 44258 | 43284 | 31608 | 41940 | 32019 | 41716 | 29160 | 41660 | 29722 |
#(!) В дату. иначе дубликатов будет меньше для теста
hh_data['Обновление резюме'] = pd.to_datetime(hh_data['Обновление резюме']).dt.date
# Переведем в категориальный тип признаки, с которыми будем работать
hh_data['Образование'] = hh_data['Образование'].astype('category')
hh_data['Город'] = hh_data['Город'].astype('category')
hh_data['Ищет работу на должность:'] = hh_data['Ищет работу на должность:'].astype('category')
Постройте распределение признака «Возраст». Опишите распределение, отвечая на следующие вопросы:
к заданию 4.1
fig = px.histogram( data_frame=hh_data,
height=540, # width=700,
title='Распределение возраста соискателей',
x='Возраст',
#nbins=100,
marginal='box',
#histnorm='percent',
).update_layout(bargap=0.05)
fig.show()
Вывод
# Информация для контроля
# print('Мода:',hh_data['Возраст'].mode()[0])
# hh_data[['Возраст']].describe()
Постройте распределение признака «Опыт работы (месяц)». Опишите распределение, отвечая на следующие вопросы:
к заданию 4.2
fig = px.histogram( data_frame=hh_data, height=540, # width=700,
title='Распределение опыта работы соскателя (месяцы)',
x='Опыт работы (месяц)',
#nbins=100,
marginal='box',
#histnorm='percent',
)#.update_layout(bargap=0.05)
fig.update_layout(bargap=0.05)
fig.show()
Вывод
# Информация для контроля
# print('Мода:', hh_data['Опыт работы (месяц)'].mode()[0])
# display(hh_data[['Опыт работы (месяц)']].describe())
Постройте распределение признака «ЗП (руб)». Опишите распределение, отвечая на следующие вопросы:
к заданию 4.3
# для лучшей визуализации немного купируем, отбросим уж совсем явные аномалии более миллиона
# в интерактивном варианте оставим как есть
# mask = hh_data['ЗП (руб)'] <= 1E6
fig = px.histogram( data_frame=hh_data , # hh_data[mask]
height=540, # width=700,
title='Распределение желаемой заработной платы соискателя (руб)',
x='ЗП (руб)',
marginal='box',
#nbins=100,
#histnorm='percent',
).update_layout(bargap=0.05)
fig.show()
# del mask
Вывод
# Информация для контроля
# print('Мода:', hh_data[mask]['ЗП (руб)'].mode()[0])
# display(hh_data[mask][['ЗП (руб)']].describe())
# hh_data[hh_data['ЗП (руб)'] > 1E6]['ЗП (руб)']
Постройте диаграмму, которая показывает зависимость медианной желаемой заработной платы («ЗП (руб)») от уровня образования («Образование»).
Используйте для диаграммы данные о резюме, где желаемая заработная плата меньше 1 миллиона рублей.
Сделайте выводы по представленной диаграмме:
к заданию 4.4
mask = hh_data['ЗП (руб)'] < 1e6
#display(hh_data[mask].groupby('Образование')[['ЗП (руб)']].median())
fig = px.bar(
data_frame=hh_data[mask].groupby('Образование')[['ЗП (руб)']].median().reset_index(),
title='Зависимость медианной желаемой заработной платы от уровня образования',
x='Образование',
y='ЗП (руб)',
color='Образование'
)
fig.show()
del mask
Вывод
# Информация для контроля
# hh_data[hh_data['ЗП (руб)'] < 1e6].groupby('Образование')[['ЗП (руб)']].median()
Постройте диаграмму, которая показывает распределение желаемой заработной платы («ЗП (руб)») в зависимости от города («Город»). Используйте для диаграммы данные о резюме, где желаемая заработная плата меньше 1 миллиона рублей.
Сделайте выводы по полученной диаграмме:
к заданию 4.5
mask = hh_data['ЗП (руб)'] < 1e6
fig = px.box(
data_frame=hh_data[mask],
title='Зависимость медианной желаемой заработной платы от места проживания',
x='ЗП (руб)',
y='Город',
color='Город'
)
fig.show()
del mask
Вывод
# Информация для контроля
Постройте многоуровневую столбчатую диаграмму, которая показывает зависимость медианной заработной платы («ЗП (руб)») от признаков «Готовность к переезду» и «Готовность к командировкам».
Проанализируйте график, сравнив уровень заработной платы по категориям.
к заданию 4.6
# сгруппируем и локализуем значения признаков
group_by = hh_data.groupby(['Готовность к переезду','Готовность к командировкам'])['ЗП (руб)'].median().reset_index()
group_by['Готовность к переезду'].replace({True:'Да', False:'Нет'}, inplace=True)
group_by['Готовность к командировкам'].replace({True:'Да', False:'Нет'}, inplace=True)
fig = px.bar(
data_frame=group_by,
title='Медианный уровень желаемой зароботной платы<br>от готовности к переезду и командировкам',
x='Готовность к командировкам',
y='ЗП (руб)',
color='Готовность к переезду',
barmode='group',
)
#fig.update_xaxes(type='category', categoryorder='category ascending')
fig.show()
del group_by
Вывод
# Информация для контроля
Постройте сводную таблицу, иллюстрирующую зависимость медианной желаемой заработной платы от возраста («Возраст») и образования («Образование»).
По полученной сводной таблице постройте тепловую карту.
Проанализируйте тепловую карту, сравнив показатели внутри групп.
к заданию 4.7
fig = px.imshow(
hh_data.pivot_table(
columns='Возраст', index='Образование',
values='ЗП (руб)', aggfunc=np.median ),
title="Тепловая карта зависимости медианной желаемой заработной платы от возраста и образования",
labels={'color':'ЗП (руб)'},
color_continuous_scale='YlOrRd', # 'Viridis' 'Blues' 'RdBu'
)
fig.show()
Вывод
# Информация для контроля
# Посмотрим на более реалистичную картинку
# mask = (hh_data['Возраст'] > 17) & (hh_data['Возраст'] < 65)
# fig = px.imshow(
# hh_data[mask].pivot_table(columns='Возраст', index='Образование', values='ЗП (руб)', aggfunc=np.median ),
# title="Тепловая карта зависимость медианной желаемой заработной платы от возраста и образования",
# labels={'color':'ЗП (руб)'},
# color_continuous_scale='YlOrRd', # 'Viridis' 'Blues' 'RdBu'
# )
# fig.show()
# del mask
Постройте диаграмму рассеяния, показывающую зависимость опыта работы («Опыт работы (месяц)») от возраста («Возраст»). Опыт работы переведите из месяцев в года, чтобы признаки были в едином масштабе.
Постройте на графике дополнительно прямую, проходящую через точки (0, 0) и (100, 100). Данная прямая соответствует значениям, когда опыт работы равен возрасту человека. Точки, лежащие на этой прямой и выше неё, — аномалии в наших данных (опыт работы больше либо равен возрасту соискателя).
к заданию 4.8
# Добавляем признак "Опыт работы" в годах и рисуем
hh_data['Опыт работы'] = (hh_data['Опыт работы (месяц)'] /12).round(1)
fig = px.scatter( data_frame=hh_data.assign(_vsize=1),
height=740, # width=820,
title='Зависимость опыта работы от возраста',
x='Возраст', y='Опыт работы', color='Образование',
opacity=0.5, size='_vsize', size_max=8,
hover_data={'_vsize':False},
range_x=[-5, 105], range_y=[-5, 105],
)
# Визуализируем "Горцев"
fig.add_trace( go.Scatter(x=[-5,105], y=[-5,105], mode='lines', name='Опыт работы равен возрасту',
line = {'color':'red','width':1.5})
)
# Визуализируем "Работников с горшка"
fig.add_trace( go.Scatter(x=[11,121], y=[-5,105], mode='lines', name='Опыт работы (Возраст-16)',
line = {'color':'orange','width':1.5})
)
fig.show()
#(!) Обязательно удаляем доп. признаки для анализа, иначе кол-во дубликатов и т.п.
# будут не соответствовать тестовым значениям. Или используем df.assign(...) для графиков
hh_data.drop(columns='Опыт работы', inplace=True)
Вывод
# Информация для контроля
дополнительное задание 4.9
# Не будем пытать уж совсем старенького дедушку
mask_age = hh_data['Возраст'] < 100
# Сформируем признак-категорию готовности
def mobile_alacrity_predict(row:pd.Series)->str:
if row['Готовность к переезду'] and row['Готовность к командировкам']:
return 'К переезду и командировкам'
elif not row['Готовность к переезду'] and row['Готовность к командировкам']:
return 'Только к переезду'
elif row['Готовность к переезду'] and not row['Готовность к командировкам']:
return 'Только к командировкам'
else:
return 'Отсутствует'
hh_data['Готовность'] = hh_data.apply(mobile_alacrity_predict, axis=1)
fig = px.histogram( data_frame= hh_data[mask_age],
height=740, # width=700,
title='Распределение возраста соскателей по группам<br>готовых к смене места жительства и командировкам',
x='Возраст',
color='Готовность',
marginal='box',
barmode='overlay',
).update_layout(bargap=0.05)
fig.show()
del mask_age
#(!) Обязательно удаляем доп. признаки для анализа, иначе кол-во дубликатов и т.п.
# будут не соответствовать тестовым значениям. Или используем df.assign(...) для графиков
hh_data.drop(columns='Готовность', inplace=True)
Вывод
дополнительное задание 4.10
# Ограничемся реалистичной минимальной зарплатой в 12т (близко к МРОТ)
# и более реалистичными зарплатами в максимуме. Топ менеджеры вряд ли ищут работу через hh.ru
mask_salary_bw = (hh_data['ЗП (руб)'] > 12_000) & (hh_data['ЗП (руб)'] < 350_000)
# Отберем в категории топ 25 искомых должностей по количеству соскателей
top25_jobs = hh_data.groupby('Ищет работу на должность:')['ЗП (руб)'] \
.agg(['count', 'median']).nlargest(25, columns='count')
# Выберем из них топ 10 самых больших ожидаемых медианных зарплат
top10_salary = top25_jobs.nlargest(10, columns='median').index
# display(top25_jobs.nlargest(10, columns='median'))
# Аналитики лидирую по количеству, руководители проектов по з/п. И это топ 10. Кризис, однако.
#отбираем и рисуем
top10_salary_mask = hh_data['Ищет работу на должность:'].isin(top10_salary)
fig = px.box(
height=720, # width=700,
data_frame=hh_data[mask_salary_bw & top10_salary_mask],
title='Распределение желаемой заработной платы в топ 10 ожидаемых<br>медианных зарплат из топ 25 искомых должностей',
x='ЗП (руб)',
y='Ищет работу на должность:',
color='Ищет работу на должность:'
).update_layout(showlegend=False)
fig.show()
del mask_salary_bw, top25_jobs, top10_salary, top10_salary_mask
Вывод
Начнём с дубликатов в наших данных. Найдите полные дубликаты в таблице с резюме и удалите их.
к заданию 5.1
#(!) Далее работаем с копией данных / перезагружать с этого места
hh_cleaned = hh_data.copy()
# Ищем дубликаты, выводим их количество
hh_duplicates = hh_cleaned[hh_cleaned.duplicated()]
print('Кол-во полных дубликатов:', hh_duplicates.shape[0])
# и удаляем. смотрим что осталось. (44744-161)
hh_cleaned.drop_duplicates(inplace=True)
print('Результирующее число записей:', hh_cleaned.shape[0])
Кол-во полных дубликатов: 161 Результирующее число записей: 44583
# Вариант с уникальными признаками (у нас нет)
#list_columns = list(hh_data.columns)
#list_columns.remove([,..]) # remove calc unq columns
#mask = hh_data.duplicated(subset=list_columns)
Итак, у нас есть пропуски в трёх столбцах: «Опыт работы (месяц)», «Последнее/нынешнее место работы», «Последняя/нынешняя должность». Поступим следующим образом:
к заданиям 5.2, 5.3
print('Признаки с пропусками:')
print('NaN`s Column')
for colm in hh_cleaned.columns:
cnt_na = hh_cleaned[colm].isna().sum()
if cnt_na: print(f'{cnt_na:5} ', colm)
Признаки с пропусками:
NaN`s Column
1 Последнее/нынешнее место работы
2 Последняя/нынешняя должность
168 Опыт работы (месяц)
# удалим столбцы с малым кол-вом пропусков
subset = ['Последнее/нынешнее место работы', 'Последняя/нынешняя должность']
hh_cleaned.dropna(subset=subset, inplace=True)
# заполняем медианым значением признак со значительными пропусками
hh_cleaned.fillna({'Опыт работы (месяц)': hh_cleaned['Опыт работы (месяц)'].median()}, inplace=True)
# Проверяем
print('Кол-во пропусков "Опыт работы":', hh_cleaned['Опыт работы (месяц)'].isna().sum())
# И в целом
print('Результирующее число записей:', hh_cleaned.shape[0])
print('Общее кол-во пропусков:',hh_cleaned.isna().sum().sum())
print('Среднее "Опыт работы (месяц)":', hh_cleaned['Опыт работы (месяц)'].mean())
Кол-во пропусков "Опыт работы": 0 Результирующее число записей: 44581 Общее кол-во пропусков: 0 Среднее "Опыт работы (месяц)": 114.35777573405711
Удалите резюме, в которых указана заработная плата либо выше 1 миллиона рублей, либо ниже 1 тысячи рублей
В процессе разведывательного анализа мы обнаружили резюме, в которых опыт работы в годах превышал возраст соискателя. Найдите такие резюме и удалите их из данных.
Найдите выбросы с помощью метода z-отклонения и удалите их из данных, используйте логарифмический масштаб.
Давайте сделаем "послабление" на 1 сигму (возьмите 4 сигмы) в правую сторону. Выведите таблицу с полученными выбросами и оцените, с каким возрастом соискатели попадают под категорию выбросов?
В какую сторону асимметрично логарифмическое распределение? Напишите об этом в комментарии к графику.
к заданиям 5.4, 5.5, 5.6
# Отфильтруем, оценим и удалим
mask_not_bw_1k_1m = (hh_cleaned['ЗП (руб)'] < 1E3) | (hh_cleaned['ЗП (руб)'] > 1E6)
print('Результирующее число записей до очистки:', hh_cleaned.shape[0])
print('Кол-во выбросов в "ЗП" менее 1K и более 1M:', hh_cleaned[mask_not_bw_1k_1m].shape[0])
hh_cleaned.drop(index=hh_cleaned[mask_not_bw_1k_1m].index, inplace=True)
del mask_not_bw_1k_1m
print('Результирующее число записей после очистки:', hh_cleaned.shape[0])
Результирующее число записей до очистки: 44581 Кол-во выбросов в "ЗП" менее 1K и более 1M: 89 Результирующее число записей после очистки: 44492
# Ищем и удаляем супер работяг "год за два" в режиме 24/7
# Можно было бы уменьшить опыт до (Возраст-16)*12, но доктор сказал - резать.
mask_work_over_age = hh_cleaned['Опыт работы (месяц)'] > hh_cleaned['Возраст']*12
print('Результирующее число записей до очистки:', hh_cleaned.shape[0])
print('Кол-во выбросов с превышением опыта работы:', hh_cleaned[mask_work_over_age].shape[0])
hh_cleaned.drop(index=hh_cleaned[mask_work_over_age].index, inplace=True)
del mask_work_over_age
print('Результирующее число записей после очистки:', hh_cleaned.shape[0])
Результирующее число записей до очистки: 44492 Кол-во выбросов с превышением опыта работы: 7 Результирующее число записей после очистки: 44485
# Очистка от выбросов в признаке "Возраст" методом трех сигм
# Сперва посмотрим распределение на графике в логарифмическом маштабе
hh_cleaned['Возраст (лог)'] = np.log(hh_cleaned['Возраст'])
# Заготовочка с курса и в право 4 сигмы
mu = hh_cleaned['Возраст (лог)'].mean()
sigma = hh_cleaned['Возраст (лог)'].std()
left_shift = 3
right_shift = 4
lower_bound = mu - left_shift * sigma
upper_bound = mu + right_shift * sigma
fig = px.histogram( data_frame=hh_cleaned,
height=540, # width=700,
title='Распределение возраста соискателей (логарифмический маштаб)',
x='Возраст (лог)',
marginal='box',
range_y=[0, 3000],
).update_layout(bargap=0.05)
# (!) Для отображения результата на графике наведите курсор в нижную часть линии
fig.add_trace( go.Scatter(x=[mu,mu], y=[0,3000], mode='lines', name='Mean',
line = {'color':'red','width':2}))
fig.add_trace( go.Scatter(x=[lower_bound, lower_bound], y=[0,3000], mode='lines', name='Lower bound',
line = {'color':'green','width':2}))
fig.add_trace( go.Scatter(x=[upper_bound, upper_bound], y=[0,3000], mode='lines', name='Upper bound',
line = {'color':'orange','width':2}))
fig.show()
# Выводим список значений выбросов
mask_log_age_emissions = (hh_cleaned['Возраст (лог)'] < lower_bound) | (hh_cleaned['Возраст (лог)'] > upper_bound)
print('Кол-во выбросов "Возраст (лог)":', mask_log_age_emissions.sum())
display(hh_cleaned[mask_log_age_emissions][['Возраст (лог)','Возраст']])
print('Нижняя граница (срд-3*сигмы):', round(np.exp(lower_bound)))
print('Верхняя граница (срд+4*сигмы):', round(np.exp(upper_bound)))
# Удостоверимся что лог. распределение лево-симмитричное, как на графике
print('Кооф. асимметрии:', round(hh_cleaned['Возраст (лог)'].skew(), 2))
#Удаляем выбросы
hh_cleaned.drop(index=hh_cleaned[mask_log_age_emissions].index, inplace=True)
del mask_log_age_emissions
hh_cleaned.drop(columns='Возраст (лог)', inplace=True)
Кол-во выбросов "Возраст (лог)": 3
| Возраст (лог) | Возраст | |
|---|---|---|
| 31137 | 2.70805 | 15 |
| 32950 | 2.70805 | 15 |
| 33654 | 4.60517 | 100 |
Нижняя граница (срд-3*сигмы): 16 Верхняя граница (срд+4*сигмы): 79 Кооф. асимметрии: 0.45
Вывод
# Итог очистки
print('Результирующее число записей до очистки:', hh_data.shape[0])
print('Результирующее число записей после очистки:', hh_cleaned.shape[0])
print('Удалено записей:', hh_data.shape[0] - hh_cleaned.shape[0])
Результирующее число записей до очистки: 44744 Результирующее число записей после очистки: 44482 Удалено записей: 262
# Сохраняем, если присутствует локальный ./data
if os.path.exists('./data'):
if os.path.exists('./data/hh_cleaned.zip'):
os.remove('./data/hh_cleaned.zip')
hh_cleaned.to_csv('./data/hh_cleaned.zip', index=False, sep=';', compression={'method':'zip','archive_name':'hh_cleaned.csv'})
### Все... Все только начинается. Переносим в пректный ноутбук